package cn.kennylee.codehub.mongodb.das.utils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import cn.kennylee.codehub.common.das.CondOperation;
import cn.kennylee.codehub.common.das.DasDbType;
import cn.kennylee.codehub.common.das.DasStrategyFactory;
import cn.kennylee.codehub.common.das.dto.PageDto;
import cn.kennylee.codehub.common.das.dto.SortDto;
import cn.kennylee.codehub.mongodb.das.entity.MongoDbEo;
import lombok.extern.slf4j.Slf4j;
import org.bson.Document;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.mongodb.core.mapping.FieldName;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;

import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

import static cn.kennylee.codehub.common.das.CondOperation.*;

/**
 * <p>MongoDB的DAS工具类</p>
 * <p>Created on 2024/12/6.</p>
 *
 * @author kennylee
 * @since 0.0.1
 */
@Slf4j
public final class MongoDbDasHelper {

    public static final String MONGO_DB_ID_NAME = FieldName.ID.name();
    public static final String PK_FIELD_NAME = MONGO_DB_ID_NAME.replace("_", "");

    /**
     * Criteria中标识“未设置的值”的对象
     */
    private volatile static Object notSetObject;

    /**
     * 模糊查询的默认配置，i=忽略大小写
     */
    public static final String REGEX_OPTIONALS = null;

    /**
     * 根据查询实例，构建分页查询，支持全部带操作符的条件
     *
     * @param pageExample 分页查询参数
     * @param eoClazz     实体类class
     * @param ignoreProps 忽略的属性列表
     * @param <T>         分页查询参数
     * @param <E>         实体类
     * @return Query
     */
    public static <E extends PageDto, P extends Serializable, T extends MongoDbEo<P>> Query buildPageQuery(final E pageExample, Class<T> eoClazz, Collection<String> ignoreProps) {
        Query query = buildQuery(pageExample, eoClazz, ignoreProps);
        // mongo从0开始分页
        int pageNo = pageExample.getPageNo() - 1;
        PageRequest pageRequest = PageRequest.of(pageNo, pageExample.getPageSize());
        query.with(pageRequest);
        return query;
    }

    public static <E extends PageDto, P extends Serializable, T extends MongoDbEo<P>> Query buildPageQuery(final E pageExample, Class<T> eoClazz) {
        return buildPageQuery(pageExample, eoClazz, Collections.emptyList());
    }

    /**
     * 构建查询
     *
     * @param searchExample 查询参数
     * @param eoClazz       实体类class
     * @param ignoreProps   忽略的属性列表
     * @param <T>           查询参数
     * @return Query
     */
    @SuppressWarnings("unchecked")
    public static <P extends Serializable, T extends MongoDbEo<P>> Query buildQuery(final Object searchExample, Class<T> eoClazz, Collection<String> ignoreProps) {
        return (Query) Objects.requireNonNull(DasStrategyFactory.getInstance().getStrategy(DasDbType.MONGODB)).parseQuery(searchExample, eoClazz, ignoreProps);
    }

    public static <P extends Serializable, T extends MongoDbEo<P>> Query buildQuery(final Object searchExample, Class<T> eoClazz) {
        return buildQuery(searchExample, eoClazz, Collections.emptyList());
    }

    /**
     * 添加模糊查询条件
     *
     * @param query     MongoDB查询条件容器
     * @param searchDto 查询参数
     * @param eoClazz   实体类class
     * @param <T>       实体类
     */
    public static <P extends Serializable, T extends MongoDbEo<P>> void addLikeConditions(Query query, Object searchDto, Class<T> eoClazz) {

        // key为属性名，下划线
        final Map<String, Object> notNullPropsMap = BeanUtil.beanToMap(searchDto, true, true);

        // like结尾的属性map，key为查询dto的原始名，value为去掉like后的eo属性名
        final Map<String, String> likeFieldMap = notNullPropsMap.keySet().stream()
            .filter(LIKE::isEndWithIgnoreCase)
            .collect(Collectors.toMap(Function.identity(), LIKE::subFieldNameUnderlineCase));

        if (log.isDebugEnabled()) {
            log.debug("like结尾的属性map: {}", JSONUtil.toJsonStr(likeFieldMap));
        }

        if (notNullPropsMap.isEmpty()) {
            return;
        }

        // 根据eo，获取能支持的属性集，转大写
        final Set<String> eoLikeProps = Arrays.stream(ReflectUtil.getFields(eoClazz))
            .map(field -> StrUtil.toUnderlineCase(field.getName()).toUpperCase())
            .collect(Collectors.toSet());

        likeFieldMap.forEach((queryPropName, eoPropName) -> {
            if (!eoLikeProps.contains(eoPropName.toUpperCase())) {
                log.debug("EO中不包含属性：{}", eoPropName);
                return;
            }
            // 获取查询dto的值
            Object val = BeanUtil.getProperty(searchDto, StrUtil.toCamelCase(queryPropName));

            final String docFieldName = StrUtil.toCamelCase(eoPropName);

            // 判断只有字符串类型才能模糊查询
            Assert.isTrue(ReflectUtil.getField(eoClazz, docFieldName).getType().equals(String.class),
                "只有字符串类型才能模糊查询");

            if (Objects.nonNull(val)) {
                if (log.isDebugEnabled()) {
                    log.debug("添加模糊查询条件，属性名：{}，属性值：{}", docFieldName, val);
                }
                addLikeCondition(query, docFieldName, val.toString());
            }
        });
    }

    /**
     * 添加模糊查询条件
     *
     * @param criteria 查询条件
     * @param key      属性名
     * @param value    属性值
     */
    public static void addLikeCondition(Criteria criteria, String key, String value) {
        if (Objects.nonNull(value)) {
            criteria.and(key).regex(toLikeRegex(value), REGEX_OPTIONALS);
        }
    }

    /**
     * 添加模糊查询条件
     * <p>注：MongoDB的查询模式，Query中只能有一个字段名的条件。多次addCriteria需要避免有相同的字段条件</p>
     *
     * @param query MongoDB查询条件容器
     * @param key   属性名
     * @param value 属性值
     */
    public static void addLikeCondition(Query query, String key, String value) {
        if (Objects.nonNull(value)) {
            query.addCriteria(Criteria.where(key).regex(toLikeRegex(value), REGEX_OPTIONALS));
        }
    }

    /**
     * 添加排序
     *
     * @param query     MongoDB查询条件容器
     * @param pageParam 分页查询参数
     * @param <E>       分页查询参数
     * @return 排序个数
     */
    public static <E extends PageDto> int addSorts(Query query, E pageParam) {
        return addSorts(query, pageParam.getSorts());
    }

    /**
     * 添加排序
     *
     * @param query MongoDB查询条件容器
     * @param sorts 排序列表
     * @return 排序个数
     */
    public static int addSorts(Query query, List<SortDto> sorts) {
        if (CollUtil.isEmpty(sorts)) {
            return 0;
        }

        int count = 0;
        for (SortDto sort : sorts) {
            //  MongoDB一般是驼峰命名
            String propName = StrUtil.toCamelCase(sort.getField());

            if (SortDto.ASC.equalsIgnoreCase(sort.getDirection())) {
                query.with(org.springframework.data.domain.Sort.by(org.springframework.data.domain.Sort.Order.asc(propName)));
                count++;
                continue;
            }
            if (SortDto.DESC.equalsIgnoreCase(sort.getDirection())) {
                query.with(org.springframework.data.domain.Sort.by(org.springframework.data.domain.Sort.Order.desc(propName)));
                count++;
            }
        }
        return count;
    }

    /**
     * 添加大于，大于等于，小于，小于等于的条件
     *
     * @param query     MongoDB查询条件容器
     * @param searchDto 查询参数
     */
    public static void addRangeConditions(Query query, Object searchDto, Class<?> eoClazz) {
        // key为属性名，下划线
        final Map<String, Object> notNullPropsMap = BeanUtil.beanToMap(searchDto, true, true);

        // like结尾的属性map，key为查询dto的原始名，value为去掉like后的eo属性名
        final Map<String, String> likeFieldMap = notNullPropsMap.keySet().stream()
            .filter(CondOperation::isRangeField)
            .collect(Collectors.toMap(Function.identity(), it -> StrUtil.subBefore(it, "_", true)));

        if (log.isDebugEnabled()) {
            log.debug("范围参数的属性map: {}", JSONUtil.toJsonStr(likeFieldMap));
        }

        if (notNullPropsMap.isEmpty()) {
            return;
        }

        // 根据eo，获取能支持的属性集，转大写
        final Set<String> eoLikeProps = Arrays.stream(ReflectUtil.getFields(eoClazz))
            .map(field -> StrUtil.toUnderlineCase(field.getName()).toUpperCase())
            .collect(Collectors.toSet());

        // MongoDB查询一个命名只能一次
        final Map<String, Criteria> propName2CriteriaMap = new LinkedHashMap<>();
        likeFieldMap.forEach((queryPropName, eoPropName) -> {
            if (!eoLikeProps.contains(eoPropName.toUpperCase())) {
                log.debug("EO中不包含属性：{}", eoPropName);
                return;
            }
            // 获取查询dto的值
            Object val = BeanUtil.getProperty(searchDto, StrUtil.toCamelCase(queryPropName));

            if (Objects.isNull(val)) {
                return;
            }

            String eoFileName = StrUtil.toCamelCase(eoPropName);
            if (log.isDebugEnabled()) {
                log.debug("添加范围查询条件，属性名：{}，属性值：{}；入参查询属性名: {}", eoFileName, val, queryPropName);
            }

            // 如果查询值是LocalDateTime，而EO中是Long类型，转换为Long
            val = toEpochMilliIfNecessary(val, eoClazz, eoFileName);

            Criteria criteria = propName2CriteriaMap.computeIfAbsent(eoFileName, k -> Criteria.where(eoFileName));

            if (GT.isEndWithIgnoreCase(queryPropName)) {
                criteria.gt(val);
            }
            if (GE.isEndWithIgnoreCase(queryPropName)) {
                criteria.gte(val);
            }
            if (LT.isEndWithIgnoreCase(queryPropName)) {
                criteria.lt(val);
            }
            if (LE.isEndWithIgnoreCase(queryPropName)) {
                criteria.lte(val);
            }
        });

        if (propName2CriteriaMap.isEmpty()) {
            return;
        }

        propName2CriteriaMap.forEach((key, value) -> {
            query.addCriteria(value);
        });
    }

    /**
     * 如果查询值是LocalDateTime，而EO中是Long类型，转换为Long
     *
     * @param val         查询值
     * @param eoClazz     EO类
     * @param eoFieldName EO属性名
     * @return 转换后的值
     */
    public static Object toEpochMilliIfNecessary(Object val, Class<?> eoClazz, String eoFieldName) {
        log.debug("查询值：{}，EO类名：{}，EO属性名：{}", val, eoClazz.getName(), eoFieldName);
        // 值是LocalDateTime
        if (val instanceof LocalDateTime localDateTimeVal &&
            // 而EO中是Long类型
            isLocationDateTimeToLong(eoClazz, eoFieldName)) {
            // 转换为Long
            return localDateTimeVal.atOffset(ZoneOffset.of("+8")).toInstant().toEpochMilli();
        }
        return val;
    }

    /**
     * 判断查询值是否是LocalDateTime，而EO中是Long类型
     *
     * @param eoClazz     EO类
     * @param eoFieldName EO属性名
     * @return 是否是LocalDateTime转Long
     */
    private static boolean isLocationDateTimeToLong(Class<?> eoClazz, String eoFieldName) {
        Class<?> eoFieldTypeClass = ReflectUtil.getField(eoClazz, eoFieldName).getType();
        if (log.isDebugEnabled()) {
            log.debug("EO属性类型：{}", eoFieldTypeClass.getName());
        }
        return eoFieldTypeClass.equals(Long.class);
    }

    /**
     * 查询值转模糊查询的正则表达式
     *
     * @param value 查询值
     * @return 模糊查询的正则表达式
     */
    public static String toLikeRegex(String value) {
        return StrUtil.format("{}", value);
    }

    /**
     * 查询值转后模糊查询的正则表达式
     *
     * @param value 查询值
     * @return 后模糊查询的正则表达式
     */
    public static String toSufLikeRegex(String value) {
        return StrUtil.format("^{}", value);
    }

    /**
     * 查询值转前模糊查询的正则表达式
     *
     * @param value 查询值
     * @return 前模糊查询的正则表达式
     */
    public static String toPreLikeRegex(String value) {
        return StrUtil.format("{}$", value);
    }

    /**
     * 添加基础唯一排序
     *
     * @param query MongoDB查询条件容器
     */
    public static void addBaseUniqueSort(Query query) {
        query.with(org.springframework.data.domain.Sort.by(org.springframework.data.domain.Sort.Order.desc(MONGO_DB_ID_NAME)));
    }

    /**
     * 判断是否是未设置的值
     *
     * @param criteria 查询条件
     * @return 是否是未设置的值
     */
    public static boolean isNotSetValue(Criteria criteria) {
        Document criteriaObject = criteria.getCriteriaObject();
        if (criteriaObject.isEmpty()) {
            return true;
        }

        String criteriaKey = criteria.getKey();
        if (Objects.isNull(criteriaKey)) {
            return true;
        }

        if (Objects.isNull(notSetObject)) {
            notSetObject = Criteria.where(criteriaKey).getCriteriaObject().get(criteriaKey);
        }

        return Objects.equals(criteriaObject.get(criteriaKey), notSetObject);
    }
}
